package cc.decat.escript.runtime;

import cc.decat.escript.codegen.BlockGenerate;
import cc.decat.escript.codegen.FunctionGenerate;
import cc.decat.escript.object.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class VM {
    private final GlobalContext globalContext;

    public VM(GlobalContext globalContext) {
        this.globalContext = globalContext;
        if(globalContext.getGlobalFunctionObject() == null) {
            ESFunctionObject functionObject = makeGlobalFunctionObject(globalContext, globalContext.getGlobalFunctionGenerate());
            globalContext.setGlobalFunctionObject(functionObject);
        }
    }

    public void execute(Context context, FunctionGenerate functionGenerate) {
        if(context.getClosure() == null) {
            context.setClosure(globalContext.getGlobalFunctionObject().generateClosure(Collections.emptyList()));
        }
        ESFunctionObject functionObject = makeFunctionObject(context, functionGenerate);
        context.push(functionObject);
        call(context, 0);
    }

    private void executeClosure(Context context, Closure closure) {
//        context.pushEnv();
        Env oldEnv = context.env;
        Closure oldClosure = context.closure;

        context.env = closure.getEnv();
        context.closure = closure;

        executeInstruction(context, closure.getFunctionObject().getFunctionGenerate().getInstructionList());

        // context.popEnv();
        context.env = oldEnv;
        context.closure = oldClosure;
    }

    private void executeInstruction(Context context, List<Instruction> instructionList) {
        int pc = 0;
        loop:
        while(pc < instructionList.size()) {
            Instruction ins = instructionList.get(pc);
            switch(ins.getOpcode()) {
                case NOP: {
                    break;
                }
                case LOAD_NULL: {
                    context.push(ESConstObjects.NULL);
                    break;
                }
                case LOAD_TRUE: {
                    context.push(ESConstObjects.TRUE);
                    break;
                }
                case LOAD_FALSE: {
                    context.push(ESConstObjects.FALSE);
                    break;
                }
                case LOAD_CONST: {
                    context.push(globalContext.getConstObjectList().get(ins.getOperand()));
                    break;
                }
                case LOAD_LOCAL_VAR: {
                    context.push(context.env.getLocalObj(ins.getOperand()));
                    break;
                }
                case LOAD_UP_VAR: {
                    context.push(context.env.getUpObj(ins.getOperand()));
                    break;
                }
                case LOAD_ATTR: {
                    ESObject rhs = context.pop();
                    ESObject lhs = context.pop();
                    context.push(lhs.getType().getAttr(lhs, rhs.str().getStr()));
                    break;
                }
                case STORE_LOCAL_VAR: {
                    context.env.setLocalObj(ins.getOperand(), context.pop());
                    break;
                }
                case STORE_UP_VAR: {
                    context.env.setUpObj(ins.getOperand(), context.pop());
                    break;
                }
                case STORE_ATTR: {
                    ESObject mhs = context.pop();
                    ESObject lhs = context.pop();
                    ESObject rhs = context.pop();
                    lhs.getType().setAttr(lhs, mhs.str().getStr(), rhs);
                    break;
                }
                case CALL_FUNCTION: {
                    call(context, ins.getOperand());
                    break;
                }
                case REMOVE_TOP: {
                    context.pop();
                    break;
                }
                case ENTRY_BLOCK: {
                    context.entryEnv(ins.getOperand());
                    break;
                }
                case LEAVE_BLOCK: {
                    context.leaveEnv();
                    break;
                }
                case BINARY_ADD:
                case BINARY_SUB:
                case BINARY_MUL:
                case BINARY_DIV:
                case BINARY_MOD:
                case COMPARE_LT:
                case COMPARE_LTE:
                case COMPARE_GT:
                case COMPARE_GTE:
                case COMPARE_EQ:
                case COMPARE_NEQ: {
                    context.push(executeBinaryOp(context.pop(), context.pop(), ins.getOpcode()));
                    break;
                }
                case MAKE_FUNCTION: {
                    ESFunctionObject functionObject = this.makeFunctionObject(context, context.getClosure().getFunctionObject().getFunctionGenerate().getChildFunctionGenerateList().get(ins.getOperand()));
                    int index = context.env.getSymbolTable().findVar(functionObject.getName()).getIndex();
                    context.env.setLocalObj(index, functionObject);
                    break;
                }
                case JUMP: {
                    pc = ins.getOperand();
                    continue loop;
                }
                case JUMP_IF_FALSE: {
                    ESObject obj = context.pop();
                    if(obj == ESConstObjects.FALSE || obj == ESConstObjects.NULL) {
                        pc = ins.getOperand();
                        continue loop;
                    }
                    break;
                }
                case JUMP_IF_FALSE_OR_POP: {
                    ESObject obj = context.peek();
                    if(obj == ESConstObjects.FALSE || obj == ESConstObjects.NULL) {
                        pc = ins.getOperand();
                        continue loop;
                    } else {
                        context.pop();
                    }
                    break;
                }
                case JUMP_IF_TRUE_OR_POP: {
                    ESObject obj = context.peek();
                    if(obj != ESConstObjects.FALSE && obj != ESConstObjects.NULL) {
                        pc = ins.getOperand();
                        continue loop;
                    } else {
                        context.pop();
                    }
                    break;
                }
                case RETURN_VALUE: {
                    return;
                }
                case BUILD_LIST: {
                    int count = ins.getOperand();
                    ESListObject listObject = new ESListObject();
                    while(count-- > 0) {
                        listObject.getList().add(context.pop());
                    }
                    Collections.reverse(listObject.getList());
                    context.push(listObject);
                    break;
                }
                case BUILD_DICT: {
                    int count = ins.getOperand();
                    ESDictObject dictObject = new ESDictObject();
                    for(int i = 0; i < count; i++) {
                        ESObject value = context.pop();
                        ESObject key = context.pop();
                        dictObject.getMap().put(key, value);
                    }
                    context.push(dictObject);
                    break;
                }
                case LOAD_SUBSCR: {
                    ESObject index = context.pop();
                    ESObject expression = context.pop();
                    context.push(expression.getType().getSubscr(expression, index));
                    break;
                }
                case STORE_SUBSCR: {
                    ESObject mhs = context.pop();
                    ESObject lhs = context.pop();
                    ESObject rhs = context.pop();
                    lhs.getType().setSubscr(lhs, mhs, rhs);
                    break;
                }
                case CONTINUE: {
                    pc = ins.getOperand();
                    while(true) {
                        if(context.env.getSymbolTable().getBlockGenerate().getType() == BlockGenerate.Type.LOOP) {
                            context.leaveEnv();
                            break;
                        }
                        context.leaveEnv();
                    }
                    continue loop;
                }
                case BREAK: {
                    pc = ins.getOperand();
                    while(context.env != null) {
                        if(context.env.getSymbolTable().getBlockGenerate().getType() == BlockGenerate.Type.LOOP) {
                            context.leaveEnv();
                            break;
                        }
                        context.leaveEnv();
                    }
                    continue loop;
                }
            }
            ++pc;
        }
    }

    private ESObject executeBinaryOp(ESObject rhs, ESObject lhs, Opcode opcode) {
        ESObject result = ESConstObjects.NULL;
        switch(opcode) {
            case BINARY_ADD:
                result = lhs.getType().add(lhs, rhs);
                break;
            case BINARY_SUB:
                result = lhs.getType().sub(lhs, rhs);
                break;
            case BINARY_MUL:
                result = lhs.getType().mul(lhs, rhs);
                break;
            case BINARY_DIV:
                result = lhs.getType().div(lhs, rhs);
                break;
            case BINARY_MOD:
                result = lhs.getType().mod(lhs, rhs);
                break;
            case COMPARE_LT:
                result = lhs.getType().lt(lhs, rhs);
                break;
            case COMPARE_LTE:
                result = lhs.getType().lte(lhs, rhs);
                break;
            case COMPARE_GT:
                result = lhs.getType().gt(lhs, rhs);
                break;
            case COMPARE_GTE:
                result = lhs.getType().gte(lhs, rhs);
                break;
            case COMPARE_EQ:
                result = lhs.getType().eq(lhs, rhs);
                break;
            case COMPARE_NEQ:
                result = lhs.getType().neq(lhs, rhs);
                break;
            default: break;
        }
        return result;
    }

    private ESFunctionObject makeGlobalFunctionObject(GlobalContext globalContext, FunctionGenerate functionGenerate) {
        ESFunctionObject functionObject = new ESNormalFunctionObject();
        functionObject.setName("__GLOBAL_FUNCTION__");
        functionObject.setFunctionGenerate(functionGenerate);
        functionObject.setClosure(makeGlobalClosure(globalContext, functionObject));
        return functionObject;
    }

    private Closure makeGlobalClosure(GlobalContext globalContext, ESFunctionObject functionObject) {
        Closure closure = new Closure();
        closure.setFunctionObject(functionObject);
        closure.setEnv(globalContext.getGlobalEnv());
        closure.setArgs(Collections.emptyList());
        closure.setParent(null);
        closure.setParentEnv(null);
        return closure;
    }

    private ESFunctionObject makeFunctionObject(Context context, FunctionGenerate functionGenerate) {
        ESFunctionObject functionObject = new ESNormalFunctionObject();
        functionObject.setName(functionGenerate.getName());
        functionObject.setFunctionGenerate(functionGenerate);
        functionObject.setClosure(makeClosure(context, functionObject));
        context.getClosure().getFunctionObject().getChildFunctionObjectList().set(functionGenerate.getIndexOfParent(), functionObject);
        return functionObject;
    }

    private Closure makeClosure(Context context, ESFunctionObject functionObject) {
        Closure closure = new Closure();
        closure.setParent(context.closure);
        closure.setFunctionObject(functionObject);
        if(context.env != null) {
            closure.setParentEnv(context.env);
        } else {
            closure.setParentEnv(globalContext.getGlobalEnv());
        }
        return closure;
    }

    private void call(Context context, int argc) {
        ESObject object = context.pop();
        List<ESObject> args = new ArrayList<>();
        while(argc-- > 0) {
            args.add(context.pop());
        }
        Collections.reverse(args);
        if(object instanceof ESFunctionObject) {
            callFunctionObject(context, (ESFunctionObject) object, args);
        } else if(object instanceof ESBuiltinObject) {
            ESObject result = ((ESBuiltinObject) object).call(args);
            context.push(result);
        }
    }

    private void callFunctionObject(Context context, ESFunctionObject functionObject, List<ESObject> args) {
        Closure closure = functionObject.generateClosure(args);
        this.executeClosure(context, closure);
    }
}
